﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MethodInfoExtensions.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the MethodInfoExtensions type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Reflection
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Windows;

    public static class MethodInfoExtensions
    {
        private static readonly FieldInfo DefaultValueConverterInstanceField = typeof(DefaultValueConverter)
            .GetField("Instance", BindingFlags.Static | BindingFlags.Public);

        private static readonly MethodInfo DefaultValueConverterChangeTypeMethod = typeof(DefaultValueConverter)
            .GetMethod("ChangeType");

        public static Action<T> CreateAction<T>(this MethodInfo targetMethod)
        {
            return (Action<T>)CreateDelegate(typeof(T), Type.EmptyTypes, targetMethod);
        }

        public static Action<T, TArg1> CreateAction<T, TArg1>(this MethodInfo targetMethod)
        {
            return (Action<T, TArg1>)CreateDelegate(typeof(T), new[] { typeof(TArg1) }, targetMethod);
        }

        public static Action<T, TArg1, TArg2> CreateAction<T, TArg1, TArg2>(this MethodInfo targetMethod)
        {
            return (Action<T, TArg1, TArg2>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2) }, targetMethod);
        }

        public static Action<T, TArg1, TArg2, TArg3> CreateAction<T, TArg1, TArg2, TArg3>(this MethodInfo targetMethod)
        {
            return (Action<T, TArg1, TArg2, TArg3>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) }, targetMethod);
        }

#if !WINDOWS_PHONE
        public static Action<T, TArg1, TArg2, TArg3, TArg4> CreateAction<T, TArg1, TArg2, TArg3, TArg4>(this MethodInfo targetMethod)
        {
            return (Action<T, TArg1, TArg2, TArg3, TArg4>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4) }, targetMethod);
        }
#endif

        public static Func<T, TReturn> CreateFunc<T, TReturn>(this MethodInfo targetMethod)
        {
            return (Func<T, TReturn>)CreateDelegate(typeof(T), Type.EmptyTypes, typeof(TReturn), targetMethod);
        }

        public static Func<T, TArg1, TReturn> CreateFunc<T, TArg1, TReturn>(this MethodInfo targetMethod)
        {
            return (Func<T, TArg1, TReturn>)CreateDelegate(typeof(T), new[] {typeof(TArg1)}, typeof(TReturn), targetMethod);
        }

        public static Func<T, TArg1, TArg2, TReturn> CreateFunc<T, TArg1, TArg2, TReturn>(this MethodInfo targetMethod)
        {
            return (Func<T, TArg1, TArg2, TReturn>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2) }, typeof(TReturn), targetMethod);
        }

        public static Func<T, TArg1, TArg2, TArg3, TReturn> CreateFunc<T, TArg1, TArg2, TArg3, TReturn>(this MethodInfo targetMethod)
        {
            return (Func<T, TArg1, TArg2, TArg3, TReturn>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3) }, typeof(TReturn), targetMethod);
        }

#if !WINDOWS_PHONE
        public static Func<T, TArg1, TArg2, TArg3, TArg4, TReturn> CreateFunc<T, TArg1, TArg2, TArg3, TArg4, TReturn>(this MethodInfo targetMethod)
        {
            return (Func<T, TArg1, TArg2, TArg3, TArg4, TReturn>)CreateDelegate(typeof(T), new[] { typeof(TArg1), typeof(TArg2), typeof(TArg3), typeof(TArg4) }, typeof(TReturn), targetMethod);
        }
#endif
        
        internal static Delegate CreateDelegate(Type sourceType, IEnumerable<Type> sourceParameterTypes, MethodInfo targetMethod)
        {
            return CreateDelegate(sourceType, sourceParameterTypes, typeof(void), targetMethod);
        }

        internal static Delegate CreateDelegate(Type sourceType, IEnumerable<Type> sourceParameterTypes, Type returnType, MethodInfo targetMethod)
        {
            var thisParameterExpression = Expression.Parameter(sourceType, "instance");

            var argumentParameterExpressions = sourceParameterTypes
                .Select((x, i) => Expression.Parameter(x, "arg" + i.ToString(CultureInfo.InvariantCulture))).ToArray();

            var targetMethodParameters = targetMethod.GetParameters();
            if (argumentParameterExpressions.Length != targetMethodParameters.Length)
            {
                throw new ArgumentException(string.Format("Incorrect number of arguments supplied for call to method '{0}'!", targetMethod));
            }

            Expression targetExpression = null;
            if (!targetMethod.IsStatic)
            {
                targetExpression = CoerceExpression(thisParameterExpression, targetMethod.DeclaringType);
            }
            
            // coerce arguments
            var callArgumentExpressions = argumentParameterExpressions
                .Select((x, i) => CoerceExpression(x, targetMethodParameters[i].ParameterType));
            
            // create delegate
            return Expression.Lambda(
                Expression.Call(
                    targetExpression, 
                    targetMethod, 
                    callArgumentExpressions), 
                new[] { thisParameterExpression }.Union(argumentParameterExpressions).ToArray()).Compile();
        }

        internal static Delegate CreateDelegate(Type sourceType, MethodInfo sourceMethod, MethodInfo targetMethod)
        {
            return CreateDelegate(sourceType, sourceMethod.GetParameters().Select(x => x.ParameterType).ToArray(),
                targetMethod);
        }

        private static Expression CoerceExpression(Expression expression, Type type)
        {
            if (type.IsAssignableFrom(expression.Type))
            {
                return expression;
            }
            
            try
            {
                return Expression.Convert(expression, type);
            }
            catch (Exception e)
            {
                throw new ArgumentException(e.Message);
            }
        }
    }
}